5장. 현실적인 선택
지금까지 데이터베이스 접근 방식은 다음과 같이 발전해왔다.
- Raw Query → 직접 제어
- Query Builder → 구조적 개선
- ORM → 객체 기반 추상화
각 방식은 이전의 문제를 해결하기 위해 등장했지만
👉 하나의 방식이 모든 상황을 해결하지는 못한다
하나로 통일할 수 없는 이유
이론적으로는 하나의 방식으로 대부분의 쿼리를 처리할 수 있다.
특히 ORM의 경우
👉 대부분의 데이터 접근을 표현하는 것이 가능하다
하지만 실제 서비스에서는 다른 문제가 발생한다.
👉 복잡해질수록 코드가 더 어려워진다
기능 vs 현실
ORM은 내부적으로 SQL을 생성하기 때문에
기능적으로는 대부분의 쿼리를 처리할 수 있다.
하지만 중요한 차이는 따로 있다.
👉 표현 방식과 제어력
복잡한 쿼리에서의 차이
다음과 같은 요구사항을 생각해보자.
- 사용자별 주문 수 집계
- 특정 개수 이상 필터링
- 주문 수 기준 정렬
SQL
SELECT u.id, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id
HAVING COUNT(o.id) >= 3
ORDER BY order_count DESC;
👉 구조가 한눈에 보인다
Query Builder
const query = knex('users as u')
.leftJoin('orders as o', 'u.id', 'o.user_id')
.groupBy('u.id');
if (minOrderCount) {
query.havingRaw('COUNT(o.id) >= ?', [minOrderCount]);
}
if (sortBy === 'orderCount') {
query.orderByRaw('COUNT(o.id) DESC');
}
const result = await query.select('u.*');
👉 SQL 구조를 유지하면서 코드로 제어 가능
ORM (Prisma)
const users = await prisma.user.groupBy({
by: ['id'],
_count: {
orders: true
},
having: {
orders: {
_count: {
gte: 3
}
}
},
orderBy: {
_count: {
orders: 'desc'
}
}
});
무엇이 문제인가
ORM도 동일한 기능을 표현할 수 있다.
하지만 코드의 특성이 달라진다.
- SQL 구조가 직접적으로 보이지 않는다
- ORM 문법을 따로 이해해야 한다
- 복잡해질수록 가독성이 떨어진다
👉 즉, 표현은 가능하지만 이해 비용이 증가한다
성능 제어의 한계
ORM은 편리하지만
실행되는 SQL에 대한 제어가 제한된다.
예를 들어
- 어떤 JOIN 방식이 사용되는지
- 어떤 인덱스가 선택되는지
- 쿼리가 몇 번 실행되는지
를 코드만 보고 파악하기 어렵다.
👉 특히 N+1 문제처럼
👉 의도하지 않은 쿼리 증가가 발생할 수 있다
또한
- index hint
- 실행 계획 제어
- 특정 DB 기능 활용
과 같은 세밀한 튜닝은 어렵거나 제한적이다.
결국 다시 SQL로 돌아간다
실무에서는 다음과 같은 흐름이 자주 발생한다.
- ORM으로 구현은 가능하지만 코드가 복잡해짐
- 성능 문제가 발생함
- 실행되는 SQL을 직접 제어해야 하는 상황 발생
이 시점에서 개발자는 자연스럽게 선택한다.
👉 “이건 그냥 SQL로 쓰는 게 낫다”
그래서 등장하는 현실적인 구조
이러한 이유로 실제 서비스에서는
하나의 방식이 아니라 여러 방식을 함께 사용한다.
👉 혼합 전략
역할에 따른 선택
각 방식은 서로 다른 역할을 가진다.
일반적인 데이터 처리 → ORM
const user = await prisma.user.findUnique({
where: { id: userId }
});
- 코드가 간결하다
- 생산성이 높다
- 구조적으로 관리하기 쉽다
👉 대부분의 CRUD 작업에 적합하다
복잡한 쿼리 구성 → Query Builder
const query = knex('users as u')
.leftJoin('orders as o', 'u.id', 'o.user_id')
.groupBy('u.id');
if (minOrderCount) {
query.havingRaw('COUNT(o.id) >= ?', [minOrderCount]);
}
if (sortBy === 'orderCount') {
query.orderByRaw('COUNT(o.id) DESC');
}
- SQL 구조를 유지하면서 코드로 제어 가능
- 동적인 쿼리 구성에 유리
- 복잡한 조건과 구조 처리에 적합
성능 / 특수 기능 → Raw Query
const result = await db.query(`
SELECT u.id, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id
`);
- 데이터베이스 기능을 그대로 활용 가능
- 성능 튜닝에 유리
- 복잡한 쿼리 표현에 가장 강력
핵심은 역할이다
이 세 가지 방식은 서로 경쟁 관계가 아니다.
👉 각각의 역할이 다르다
- ORM → 생산성
- Query Builder → 유연성
- Raw Query → 제어력
잘못된 접근 방식
다음과 같은 접근은 오히려 문제를 만든다.
- “ORM만 사용해야 한다”
- “Raw Query는 나쁜 방식이다”
- “하나로 통일해야 한다”
이런 선택은
👉 특정 상황에서 비효율적인 구조를 만들게 된다
선택 기준
실제 선택은 다음 기준으로 이루어진다.
- 쿼리의 복잡도
- 성능 요구사항
- 유지보수 용이성
- 팀의 이해도
정리
데이터베이스 접근 방식은
하나의 정답이 있는 문제가 아니다.
👉 중요한 것은 기술 자체가 아니라
👉 상황에 맞는 선택이다